package logger

import (
	"fmt"
	"os"
	"path/filepath"
	"strings"

	"github.com/go-kratos/kratos/v2/log"
	"go.uber.org/zap"
	"go.uber.org/zap/zapcore"
	"gopkg.in/natefinch/lumberjack.v2"
)

var _ log.Logger = (*ZapLogger)(nil)

const (
	consoleEncoder = "console"
	jsonEncoder    = "json"
)

var (
	fileLevels = []zapcore.Level{zapcore.DebugLevel, zapcore.InfoLevel, zapcore.WarnLevel, zapcore.ErrorLevel}
)

type LogConfig struct {
	Filename   string
	Level      string
	Encoder    string
	MultiFile  bool
	Files      string
	MaxSize    int
	MaxBackups int
	MaxAge     int
}

// ZapLogger is a logger impl.
type ZapLogger struct {
	log  *zap.Logger
	Sync func() error
}

// NewZapLogger return a zap logger.
func NewZapLogger(conf *LogConfig, opts ...zap.Option) *ZapLogger {
	level, err := zapcore.ParseLevel(conf.Level)
	if err != nil {
		log.Fatalf("log level error: %v", err)
	}
	var cores []zapcore.Core
	if conf.MultiFile && conf.Filename != "" {
		cores = getMultiCore(conf, level)
	} else {
		cores = getSingleCore(conf, level)
	}
	// zap.AddStacktrace(
	// 	zap.NewAtomicLevelAt(zapcore.ErrorLevel)),
	// zap.AddCaller(),
	// zap.AddCallerSkip(2),
	zapLogger := zap.New(zapcore.NewTee(cores...), opts...)
	return &ZapLogger{log: zapLogger, Sync: zapLogger.Sync}
}

// Log Implementation of logger interface.
func (l *ZapLogger) Log(level log.Level, keyvals ...interface{}) error {
	if len(keyvals) == 0 || len(keyvals)%2 != 0 {
		l.log.Warn(fmt.Sprint("Keyvalues must appear in pairs: ", keyvals))
		return nil
	}
	// Zap.Field is used when keyvals pairs appear
	var data []zap.Field
	for i := 0; i < len(keyvals); i += 2 {
		data = append(data, zap.Any(fmt.Sprint(keyvals[i]), fmt.Sprint(keyvals[i+1])))
	}
	switch level {
	case log.LevelDebug:
		l.log.Debug("", data...)
	case log.LevelInfo:
		l.log.Info("", data...)
	case log.LevelWarn:
		l.log.Warn("", data...)
	case log.LevelError:
		l.log.Error("", data...)
	}
	return nil
}

func getSingleCore(conf *LogConfig, level zapcore.Level) []zapcore.Core {
	encoder := getEncoder(conf.Encoder)
	writes := getWrites(conf, "")
	syncer := zapcore.NewMultiWriteSyncer(writes...)
	core := zapcore.NewCore(encoder, syncer, level)
	return []zapcore.Core{
		core,
	}
}

func getMultiCore(conf *LogConfig, level zapcore.Level) []zapcore.Core {
	levels := parseLevels(conf.Files)
	if len(levels) == 0 {
		levels = fileLevels
	}
	cores := make([]zapcore.Core, 0, len(levels))
	encoder := getEncoder(conf.Encoder)
	var priority zap.LevelEnablerFunc
	for i := 0; i < len(levels); i++ {
		l := levels[i]
		if l < level {
			continue
		}
		if l < zapcore.ErrorLevel {
			priority = zap.LevelEnablerFunc(func(lev zapcore.Level) bool {
				return lev == l
			})
		} else {
			priority = zap.LevelEnablerFunc(func(lev zapcore.Level) bool { //error级别
				return lev >= l
			})
		}
		writers := getWrites(conf, l.String())
		core := zapcore.NewCore(encoder, zapcore.NewMultiWriteSyncer(writers...), priority)
		cores = append(cores, core)
	}
	return cores
}

func getWrites(conf *LogConfig, level string) []zapcore.WriteSyncer {
	// 默认标准输出
	ws := []zapcore.WriteSyncer{
		getStdoutWrite(),
	}
	if conf.Filename != "" {
		filename := conf.Filename
		if conf.MultiFile {
			filename = getMultiFileName(filename, level)
		}
		if conf.MaxSize > 0 {
			ws = append(ws, getRotateWrite(filename, conf.MaxSize, conf.MaxBackups, conf.MaxAge))
		} else {
			ws = append(ws, getFileWrite(filename))
		}
	}
	return ws
}

// logs/xxx.log => logs/error.log
// logs/xxx => logs/xxx/error.log
func getMultiFileName(path string, level string) string {
	if level == "" {
		level = "unknown"
	}
	ext := filepath.Ext(path)
	if ext != "" {
		return filepath.Join(filepath.Dir(path), level+ext)
	}
	return filepath.Join(path, level+".log")
}

func parseLevels(file string) []zapcore.Level {
	if file == "" {
		return nil
	}
	var levels []zapcore.Level
	files := strings.Split(file, ",")
	for _, v := range files {
		level, err := zapcore.ParseLevel(v)
		if err != nil {
			log.Fatalf("log level error: %v", err)
			continue
		}
		if level > zapcore.ErrorLevel {
			continue
		}
		levels = append(levels, level)
	}
	return levels
}

func getEncoder(mode string) zapcore.Encoder {
	config := zap.NewProductionEncoderConfig()
	config.EncodeTime = zapcore.ISO8601TimeEncoder
	config.EncodeLevel = zapcore.CapitalLevelEncoder
	if mode == jsonEncoder {
		return zapcore.NewJSONEncoder(config)
	}
	return zapcore.NewConsoleEncoder(config)
}

func getStdoutWrite() zapcore.WriteSyncer {
	return zapcore.AddSync(os.Stdout)
}

func getFileWrite(filename string) zapcore.WriteSyncer {
	file, err := os.OpenFile(filename, os.O_CREATE|os.O_APPEND, 0664)
	if err != nil {
		log.Fatalf("cannot open log file: %v", err)
	}
	return zapcore.AddSync(file)
}

func getRotateWrite(filename string, maxSize int, maxBackups int, maxAge int) zapcore.WriteSyncer {
	r := &lumberjack.Logger{
		Filename:   filename,
		MaxSize:    maxSize,    //20 megabytes
		MaxBackups: maxBackups, //15 backup files
		MaxAge:     maxAge,     //30 days
	}
	return zapcore.AddSync(r)
}
